package com.googlecode.horncomb.orm.id;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

import org.springframework.beans.BeanUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.googlecode.horncomb.bean.id.Identifiable;

public abstract class EntityIdUtil {
	private static Map<Class<?>, Field> idFieldCache = new HashMap<Class<?>, Field>();
	private static Map<Class<?>, PropertyDescriptor> idPropertyDescriptorCache = new HashMap<Class<?>, PropertyDescriptor>();
	private static Map<Class<?>, String> idNameCache = new HashMap<Class<?>, String>();

	public static void initIdLocationCacheIfNeed(Class<?> entityClass,
			String defaultIdName) {
		initIdLocationCache(entityClass, defaultIdName, false);
	}

	/**
	 * @param entityClass
	 * @param defaultIdName
	 * @param forceInit
	 *            if force load whatever it's cached.\
	 * @return 是否成功找到Id
	 */
	public static boolean initIdLocationCache(Class<?> entityClass,
			String defaultIdName, boolean forceInit) {
		Assert.notNull(entityClass);
		Field cachedIdField = null;
		PropertyDescriptor cachedIdPropertyDescriptor = null;
		String cachedIdName = null;
		if (!forceInit) {
			cachedIdField = getIdFieldFromCache(entityClass);
			cachedIdPropertyDescriptor = getIdPropertyDescriptorFromCache(entityClass);
			cachedIdName = getIdNameFromCache(entityClass);
		}
		// force or not cached
		if (forceInit
				|| ((cachedIdField == null && cachedIdPropertyDescriptor == null) || !StringUtils
						.hasText(cachedIdName))) {
			Object idLoc = locatingIdType(entityClass, defaultIdName);
			idFieldCache.remove(entityClass);
			idPropertyDescriptorCache.remove(entityClass);
			idNameCache.remove(entityClass);
			if (idLoc != null) {
				if (idLoc instanceof Field) {
					Field idField = (Field) idLoc;
					putIdFieldIntoCache(entityClass, idField);
				} else if (idLoc instanceof PropertyDescriptor) {
					PropertyDescriptor idPropertyDescriptor = (PropertyDescriptor) idLoc;
					putIdPropertyDescriptorIntoCache(entityClass,
							idPropertyDescriptor);
				}
				return true;
			} else {
				return false;
			}
		} else {
			return true;
		}
	}

	public static Field getIdFieldFromCache(Class<?> entityClass) {
		return idFieldCache.get(entityClass);
	}

	/**
	 * cache field and validate newIdField is matched with cached property name.
	 * @param entityClass
	 * @param newIdField
	 */
	public static void putIdFieldIntoCache(Class<?> entityClass,
			Field newIdField) {
		Assert.notNull(entityClass);
		Assert.notNull(newIdField);
		String newIdName = newIdField.getName();
		String cachedIdName = getIdNameFromCache(entityClass);
		if (!StringUtils.hasText(cachedIdName)) {
			idNameCache.put(entityClass, newIdName);
		} else if (!cachedIdName.equals(newIdName)) {
			throw new IllegalStateException("new field[" + newIdName
					+ "] mismatch with cached id name[" + cachedIdName
					+ "] for type[" + entityClass.getName() + "]");
		}
		idFieldCache.put(entityClass, newIdField);
		idPropertyDescriptorCache.remove(entityClass);
	}

	public static PropertyDescriptor getIdPropertyDescriptorFromCache(
			Class<?> entityClass) {
		return idPropertyDescriptorCache.get(entityClass);
	}

	/**
	 * cache newIdPropertyDescriptor and validate newIdPropertyDescriptor is
	 * matched with cached property name.
	 * @param entityClass
	 * @param newIdPropertyDescriptor
	 */
	public static void putIdPropertyDescriptorIntoCache(Class<?> entityClass,
			PropertyDescriptor newIdPropertyDescriptor) {
		Assert.notNull(entityClass);
		Assert.notNull(newIdPropertyDescriptor);
		String newIdName = newIdPropertyDescriptor.getDisplayName();
		String cachedIdName = getIdNameFromCache(entityClass);
		if (!StringUtils.hasText(cachedIdName)) {
			idNameCache.put(entityClass, newIdName);
		} else if (!cachedIdName.equals(newIdName)) {
			throw new IllegalStateException("new property[" + newIdName
					+ "] mismatch with cached id name[" + cachedIdName
					+ "] for type[" + entityClass.getName() + "]");
		}
		idPropertyDescriptorCache.put(entityClass, newIdPropertyDescriptor);
		idFieldCache.remove(entityClass);
	}

	public static boolean hasIdInCache(Class<?> entityClass) {
		Field cachedIdField = getIdFieldFromCache(entityClass);
		PropertyDescriptor cachedIdPropertyDescriptor = getIdPropertyDescriptorFromCache(entityClass);
		String cachedIdName = getIdNameFromCache(entityClass);
		if ((cachedIdField == null && cachedIdPropertyDescriptor == null)
				|| !StringUtils.hasText(cachedIdName)) {
			return false;
		} else {
			return true;
		}
	}

	/**
	 * get
	 * @param entityClass
	 * @return
	 */
	public static String getIdNameFromCache(Class<?> entityClass) {
		return idNameCache.get(entityClass);
	}

	// /**
	// * put the name of id into cache
	// * @param entityClass
	// * @param idName
	// */
	// public static void putIdNameIntoCache(Class<?> entityClass, String
	// idName) {
	// Assert.notNull(entityClass);
	// Assert.hasLength(idName);
	// Field cachedIdField = getIdFieldFromCache(entityClass);
	// PropertyDescriptor cachedIdPropertyDescriptor =
	// getIdPropertyDescriptorFromCache(entityClass);
	// if (cachedIdField == null && cachedIdPropertyDescriptor == null) {
	// initIdLocationCacheIfNeed(entityClass, idName);
	// } else if (cachedIdField != null) {
	// // had cached field.
	// // validate cached Field matched with new property name.
	// if (!cachedIdField.getName().equals(idName))
	// throw new IllegalStateException("new id name[" + idName
	// + "] mismatch with cached Field["
	// + cachedIdField.getName() + "] for type["
	// + entityClass.getName() + "]");
	// } else if (cachedIdPropertyDescriptor != null) {
	// // had cached property descriptor.
	// // validate cached property descriptor matched with new property
	// // name.
	// if (!cachedIdPropertyDescriptor.getDisplayName().equals(idName))
	// throw new IllegalStateException("new id name[" + idName
	// + "] mismatch with cached propertyDescritor["
	// + cachedIdPropertyDescriptor.getDisplayName()
	// + "] for type[" + entityClass.getName() + "]");
	// }
	//
	// }

	/**
	 * locate the id.
	 * @param entityClass
	 * @param idName
	 *            the name of id field for candidate
	 * @return
	 */
	public static Object locatingIdType(Class<?> entityClass, String idName) {
		Assert.notNull(entityClass);
		Object result = null;

		// 如果继承了Identifiable接口
		if (Identifiable.class.isAssignableFrom(entityClass)) {
			result = BeanUtils.getPropertyDescriptor(entityClass, "id");
		}

		// 被标识为@Entity
		if (result == null) {
			if (!entityClass.isAnnotationPresent(Entity.class)
					&& !entityClass.isAnnotationPresent(MappedSuperclass.class)) {
				Class<?> superClz = entityClass;
				do {
					superClz = superClz.getSuperclass();
				} while (superClz != null
						&& !superClz.isAnnotationPresent(Entity.class)
						&& !superClz
								.isAnnotationPresent(MappedSuperclass.class)
						&& !Object.class.equals(superClz));
				if (superClz != null
						&& (superClz.isAnnotationPresent(Entity.class) || superClz
								.isAnnotationPresent(MappedSuperclass.class))) {
					Field cachedIdField = getIdFieldFromCache(superClz);
					PropertyDescriptor cachedIdPropertyDescriptor = getIdPropertyDescriptorFromCache(superClz);
					result = (cachedIdField != null ? cachedIdField
							: (cachedIdPropertyDescriptor != null ? cachedIdPropertyDescriptor
									: null));
				}
			}
			if (result == null) {
				result = locatingIdTypeByAnnotation(entityClass);
			}
		}

		if (result == null && StringUtils.hasText(idName)) {
			// find in property (get method)
			try {
				result = BeanUtils.getPropertyDescriptor(entityClass, idName);
			} catch (Exception ex) { // ignore
			}
		}

		// if (result == null) {
		// throw new NullPointerException("can not find id property in type["
		// + entityClass + "]");
		// } else {
		// return result;
		// }
		return result;
	}

	/**
	 * 根据Annotation获取Id位置
	 * @param entityClass
	 * @return
	 */
	public static Object locatingIdTypeByAnnotation(Class<?> entityClass) {
		Object result = null;
		if (entityClass.isAnnotationPresent(Entity.class)
				|| entityClass.isAnnotationPresent(MappedSuperclass.class)) {
			for (PropertyDescriptor pd : BeanUtils
					.getPropertyDescriptors(entityClass)) {
				if (pd.getReadMethod() != null
						&& pd.getReadMethod().isAnnotationPresent(Id.class)) {
					result = pd;
					break;
				}
			}
			for (Field field : entityClass.getDeclaredFields()) {
				if (field.isAnnotationPresent(Id.class)) {
					result = field;
					break;
				}
			}
			if (result == null) {
				if (!entityClass.isInterface()) {
					// 递归查找父类
					Class<?> superClass = entityClass.getSuperclass();
					if (superClass != null
							&& superClass
									.isAnnotationPresent(MappedSuperclass.class)) {
						result = locatingIdTypeByAnnotation(superClass);
					}
				}
				if (result == null) {
					// 从接口中查找
					Class<?>[] interfaceClasses = entityClass.getInterfaces();
					if (interfaceClasses != null && interfaceClasses.length > 0) {
						for (Class<?> interfaceClass : interfaceClasses) {
							if (interfaceClass
									.isAnnotationPresent(MappedSuperclass.class)) {
								result = locatingIdTypeByAnnotation(interfaceClass);
								if (result != null) {
									break;
								}
							}
						}
					}
				}
			}
		}
		return result;
	}

	public static Serializable getIdValue(Object entity)
			throws IllegalArgumentException, IllegalAccessException,
			NoSuchMethodException, InvocationTargetException,
			InstantiationException {
		return getIdValue(entity, null);
	}

	public static Serializable getIdValue(Object entity, String defaultIdName)
			throws IllegalArgumentException, IllegalAccessException,
			NoSuchMethodException, InvocationTargetException,
			InstantiationException {
		initIdLocationCacheIfNeed(entity.getClass(), defaultIdName);
		return getIdValue(entity, getIdFieldFromCache(entity.getClass()),
				getIdPropertyDescriptorFromCache(entity.getClass()));
	}

	/**
	 * 根据实体对象获得这个实体的Id的值
	 * @param entity
	 * @param idField
	 * @param idPropertyDescriptor
	 * @return
	 * @throws NoSuchMethodException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws InstantiationException
	 */
	public static Serializable getIdValue(Object entity, Field idField,
			PropertyDescriptor idPropertyDescriptor)
			throws NoSuchMethodException, IllegalAccessException,
			InvocationTargetException, InstantiationException {
		Assert.notNull(entity);
		// 判断是否是Entity接口类型
		if (entity instanceof Identifiable) {
			return ((Identifiable<?>) entity).getId();
		} else {
			Assert.isTrue(idField != null || idPropertyDescriptor != null,
					"idField or idPropertyDescriptor must not be null.");
			if (idField != null) {
				if (!idField.isAccessible()) {
					idField.setAccessible(true);
				}
				return (Serializable) idField.get(entity);
			} else {
				Method readMethod = idPropertyDescriptor.getReadMethod();
				if (!readMethod.isAccessible()) {
					readMethod.setAccessible(true);
				}
				return (Serializable) readMethod.invoke(entity);
			}
		}
	}

	public static void setIdValue(Object entity, Serializable idValue)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException, NoSuchMethodException {
		setIdValue(entity, idValue, null);
	}

	public static void setIdValue(Object entity, Serializable idValue,
			String defaultIdName) throws IllegalArgumentException,
			IllegalAccessException, InvocationTargetException,
			NoSuchMethodException {
		initIdLocationCacheIfNeed(entity.getClass(), defaultIdName);
		setIdValue(entity, idValue, getIdFieldFromCache(entity.getClass()),
				getIdPropertyDescriptorFromCache(entity.getClass()));
	}

	/**
	 * 设置Id的值
	 * @param entity
	 * @param idValue
	 * @throws NoSuchMethodException
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws InstantiationException
	 */
	@SuppressWarnings("unchecked")
	public static void setIdValue(Object entity, Serializable idValue,
			Field idField, PropertyDescriptor idPropertyDescriptor)
			throws IllegalAccessException, InvocationTargetException,
			NoSuchMethodException {
		Assert.notNull(entity);
		if (entity instanceof Identifiable) {
			Identifiable e = (Identifiable) entity;
			e.setId(idValue);
		} else {
			Assert.isTrue(idField != null || idPropertyDescriptor != null,
					"idField or idPropertyDescriptor must not be null.");
			if (idField != null) {
				idField.set(entity, idValue);
			} else if (idPropertyDescriptor != null) {
				Assert.notNull(idPropertyDescriptor);
				Method writeMethod = idPropertyDescriptor.getWriteMethod();
				if (!writeMethod.isAccessible()) {
					writeMethod.setAccessible(true);
				}
				writeMethod.invoke(entity, idValue);
			}
		}
	}

}
